package com.winit.schedule.task.tracker.schema;

import java.util.LinkedHashSet;
import java.util.Set;

import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionDefaults;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.AnnotationScopeMetadataResolver;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Role;
import org.springframework.context.annotation.ScannedGenericBeanDefinition;
import org.springframework.context.annotation.ScopeMetadata;
import org.springframework.context.annotation.ScopeMetadataResolver;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.Assert;
import org.springframework.util.PatternMatchUtils;

import com.winit.schedule.core.task.ScheduleTaskDefine;
import com.winit.schedule.core.task.annotation.ScheduleTask;
import com.winit.schedule.core.utils.StringUtils;

/**
 * A bean definition scanner that detects bean candidates on the classpath,
 * registering corresponding bean definitions with a given registry (
 * {@code BeanFactory} or {@code ApplicationContext}).
 * <p>
 * Candidate classes are detected through configurable type filters. The default
 * filters include classes that are annotated with Spring's
 * {@link org.springframework.stereotype.Component @Component},
 * {@link org.springframework.stereotype.Repository @Repository},
 * {@link org.springframework.stereotype.Service @Service}, or
 * {@link org.springframework.stereotype.Controller @Controller} stereotype.
 * <p>
 * Also supports Java EE 6's {@link javax.annotation.ManagedBean} and JSR-330's
 * {@link javax.inject.Named} annotations, if available.
 * 
 * @author Mark Fisher
 * @author Juergen Hoeller
 * @author Chris Beams
 * @since 2.5
 * @see AnnotationConfigApplicationContext#scan
 * @see org.springframework.stereotype.Component
 * @see org.springframework.stereotype.Repository
 * @see org.springframework.stereotype.Service
 * @see org.springframework.stereotype.Controller
 */
public class UScheduleClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {

    private final BeanDefinitionRegistry registry;

    private BeanDefinitionDefaults       beanDefinitionDefaults  = new BeanDefinitionDefaults();

    private String[]                     autowireCandidatePatterns;

    private BeanNameGenerator            beanNameGenerator       = new AnnotationBeanNameGenerator();

    private ScopeMetadataResolver        scopeMetadataResolver   = new AnnotationScopeMetadataResolver();

    private boolean                      includeAnnotationConfig = true;

    /**
     * Create a new {@code ClassPathBeanDefinitionScanner} for the given bean
     * factory.
     * 
     * @param registry the {@code BeanFactory} to load bean definitions into, in
     * the form of a {@code BeanDefinitionRegistry}
     */
    public UScheduleClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry){
        this(registry, true);
    }

    /**
     * Create a new {@code ClassPathBeanDefinitionScanner} for the given bean
     * factory.
     * <p>
     * If the passed-in bean factory does not only implement the
     * {@code BeanDefinitionRegistry} interface but also the
     * {@code ResourceLoader} interface, it will be used as default
     * {@code ResourceLoader} as well. This will usually be the case for
     * {@link org.springframework.context.ApplicationContext} implementations.
     * <p>
     * If given a plain {@code BeanDefinitionRegistry}, the default
     * {@code ResourceLoader} will be a
     * {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver}.
     * <p>
     * If the the passed-in bean factory also implements
     * {@link EnvironmentCapable} its environment will be used by this reader.
     * Otherwise, the reader will initialize and use a
     * {@link org.springframework.core.env.StandardEnvironment}. All
     * {@code ApplicationContext} implementations are {@code EnvironmentCapable}
     * , while normal {@code BeanFactory} implementations are not.
     * 
     * @param registry the {@code BeanFactory} to load bean definitions into, in
     * the form of a {@code BeanDefinitionRegistry}
     * @param useDefaultFilters whether to include the default filters for the
     * {@link org.springframework.stereotype.Component @Component},
     * {@link org.springframework.stereotype.Repository @Repository},
     * {@link org.springframework.stereotype.Service @Service}, and
     * {@link org.springframework.stereotype.Controller @Controller} stereotype
     * annotations.
     * @see #setResourceLoader
     * @see #setEnvironment
     */
    public UScheduleClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters){
        this(registry, useDefaultFilters, getOrCreateEnvironment(registry));
    }

    /**
     * Create a new {@code ClassPathBeanDefinitionScanner} for the given bean
     * factory and using the given {@link Environment} when evaluating bean
     * definition profile metadata.
     * <p>
     * If the passed-in bean factory does not only implement the
     * {@code BeanDefinitionRegistry} interface but also the
     * {@link ResourceLoader} interface, it will be used as default
     * {@code ResourceLoader} as well. This will usually be the case for
     * {@link org.springframework.context.ApplicationContext} implementations.
     * <p>
     * If given a plain {@code BeanDefinitionRegistry}, the default
     * {@code ResourceLoader} will be a
     * {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver}.
     * 
     * @param registry the {@code BeanFactory} to load bean definitions into, in
     * the form of a {@code BeanDefinitionRegistry}
     * @param useDefaultFilters whether to include the default filters for the
     * @param environment the Spring {@link Environment} to use when evaluating
     * bean definition profile metadata.
     * {@link org.springframework.stereotype.Component @Component},
     * {@link org.springframework.stereotype.Repository @Repository},
     * {@link org.springframework.stereotype.Service @Service}, and
     * {@link org.springframework.stereotype.Controller @Controller} stereotype
     * annotations.
     * @since 3.1
     * @see #setResourceLoader
     */
    public UScheduleClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
                                                   Environment environment){
        super(useDefaultFilters, environment);

        Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
        this.registry = registry;

        // Determine ResourceLoader to use.
        if (this.registry instanceof ResourceLoader) {
            setResourceLoader((ResourceLoader) this.registry);
        }
    }

    /**
     * Return the BeanDefinitionRegistry that this scanner operates on.
     */
    public final BeanDefinitionRegistry getRegistry() {
        return this.registry;
    }

    /**
     * Set the defaults to use for detected beans.
     * 
     * @see BeanDefinitionDefaults
     */
    public void setBeanDefinitionDefaults(BeanDefinitionDefaults beanDefinitionDefaults) {
        this.beanDefinitionDefaults = (beanDefinitionDefaults != null ? beanDefinitionDefaults : new BeanDefinitionDefaults());
    }

    /**
     * Set the name-matching patterns for determining autowire candidates.
     * 
     * @param autowireCandidatePatterns the patterns to match against
     */
    public void setAutowireCandidatePatterns(String[] autowireCandidatePatterns) {
        this.autowireCandidatePatterns = autowireCandidatePatterns;
    }

    /**
     * Set the BeanNameGenerator to use for detected bean classes.
     * <p>
     * Default is a {@link AnnotationBeanNameGenerator}.
     */
    public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) {
        this.beanNameGenerator = (beanNameGenerator != null ? beanNameGenerator : new AnnotationBeanNameGenerator());
    }

    /**
     * Set the ScopeMetadataResolver to use for detected bean classes. Note that
     * this will override any custom "scopedProxyMode" setting.
     * <p>
     * The default is an {@link AnnotationScopeMetadataResolver}.
     * 
     * @see #setScopedProxyMode
     */
    public void setScopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver) {
        this.scopeMetadataResolver = (scopeMetadataResolver != null ? scopeMetadataResolver : new AnnotationScopeMetadataResolver());
    }

    /**
     * Specify the proxy behavior for non-singleton scoped beans. Note that this
     * will override any custom "scopeMetadataResolver" setting.
     * <p>
     * The default is {@link ScopedProxyMode#NO}.
     * 
     * @see #setScopeMetadataResolver
     */
    public void setScopedProxyMode(ScopedProxyMode scopedProxyMode) {
        this.scopeMetadataResolver = new AnnotationScopeMetadataResolver(scopedProxyMode);
    }

    /**
     * Specify whether to register annotation config post-processors.
     * <p>
     * The default is to register the post-processors. Turn this off to be able
     * to ignore the annotations or to process them differently.
     */
    public void setIncludeAnnotationConfig(boolean includeAnnotationConfig) {
        this.includeAnnotationConfig = includeAnnotationConfig;
    }

    /**
     * Perform a scan within the specified base packages, returning the
     * registered bean definitions.
     * <p>
     * This method does <i>not</i> register an annotation config processor but
     * rather leaves this up to the caller.
     * 
     * @param basePackages the packages to check for annotated classes
     * @return set of beans registered if any for tooling registration purposes
     * (never {@code null})
     */
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Assert.notEmpty(basePackages, "At least one base package must be specified");
        Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
        for (String basePackage : basePackages) {
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
            for (BeanDefinition candidate : candidates) {
                ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
                candidate.setScope(scopeMetadata.getScopeName());
                String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
                if (candidate instanceof AbstractBeanDefinition) {
                    postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
                }
                if (candidate instanceof AnnotatedBeanDefinition) {
                    processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
                }

                if (checkCandidate(beanName, candidate)) {

                    if (candidate instanceof AnnotatedBeanDefinition) {
                        AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidate;
                        if (beanDefinition.getMetadata().isAnnotated(ScheduleTask.class.getName())) {
                            @SuppressWarnings("rawtypes")
                            Class beanClass = null;
                            try {
                                beanClass = Class.forName(beanDefinition.getBeanClassName());
                            } catch (ClassNotFoundException e) {
                                e.printStackTrace();
                                logger.error("Class.forName failure, ", e);
                            }
                            @SuppressWarnings("unchecked")
                            ScheduleTask service = (ScheduleTask) beanClass.getAnnotation(ScheduleTask.class);
                            String _beanName = service.taskName();

                            if (StringUtils.isBlank(_beanName)) {
                                _beanName = beanDefinition.getBeanClassName();
                            }

                            _beanName = ScheduleTaskDefine.BEAN_PREFIX + _beanName;

                            RootBeanDefinition rootBeanDefinition = new RootBeanDefinition();
                            rootBeanDefinition.setBeanClass(beanClass);
                            rootBeanDefinition.setLazyInit(false);

                            this.registry.registerBeanDefinition(_beanName, rootBeanDefinition);
                        }
                    }

                }
            }
        }
        return beanDefinitions;
    }

    static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd) {
        AnnotationMetadata metadata = abd.getMetadata();
        if (metadata.isAnnotated(Primary.class.getName())) {
            abd.setPrimary(true);
        }
        if (metadata.isAnnotated(Lazy.class.getName())) {
            abd.setLazyInit(attributesFor(metadata, Lazy.class).getBoolean("value"));
        }
        if (metadata.isAnnotated(DependsOn.class.getName())) {
            abd.setDependsOn(attributesFor(metadata, DependsOn.class).getStringArray("value"));
        }
        if (abd instanceof AbstractBeanDefinition) {
            if (metadata.isAnnotated(Role.class.getName())) {
                Integer role = attributesFor(metadata, Role.class).getNumber("value");
                ((AbstractBeanDefinition) abd).setRole(role);
            }
        }
    }

    public static AnnotationAttributes attributesFor(AnnotationMetadata metadata, Class<?> annoClass) {
        return attributesFor(metadata, annoClass.getName());
    }

    public static AnnotationAttributes attributesFor(AnnotationMetadata metadata, String annoClassName) {
        return AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(annoClassName, false));
    }

    /**
     * Apply further settings to the given bean definition, beyond the contents
     * retrieved from scanning the component class.
     * 
     * @param beanDefinition the scanned bean definition
     * @param beanName the generated bean name for the given bean
     */
    protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) {
        beanDefinition.applyDefaults(this.beanDefinitionDefaults);
        if (this.autowireCandidatePatterns != null) {
            beanDefinition.setAutowireCandidate(PatternMatchUtils.simpleMatch(this.autowireCandidatePatterns, beanName));
        }
    }

    /**
     * Register the specified bean with the given registry.
     * <p>
     * Can be overridden in subclasses, e.g. to adapt the registration process
     * or to register further bean definitions for each scanned bean.
     * 
     * @param definitionHolder the bean definition plus bean name for the bean
     * @param registry the BeanDefinitionRegistry to register the bean with
     */
    protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) {
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
    }

    /**
     * Check the given candidate's bean name, determining whether the
     * corresponding bean definition needs to be registered or conflicts with an
     * existing definition.
     * 
     * @param beanName the suggested name for the bean
     * @param beanDefinition the corresponding bean definition
     * @return <code>true</code> if the bean can be registered as-is;
     * <code>false</code> if it should be skipped because there is an existing,
     * compatible bean definition for the specified name
     * @throws ConflictingBeanDefinitionException if an existing, incompatible
     * bean definition has been found for the specified name
     */
    protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
        if (!this.registry.containsBeanDefinition(beanName)) {
            return true;
        }
        BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);
        BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
        if (originatingDef != null) {
            existingDef = originatingDef;
        }
        if (isCompatible(beanDefinition, existingDef)) {
            return false;
        }
        throw new IllegalArgumentException("Annotation-specified bean name '" + beanName + "' for bean class ["
                                           + beanDefinition.getBeanClassName() + "] conflicts with existing, "
                                           + "non-compatible bean definition of same name and class ["
                                           + existingDef.getBeanClassName() + "]");
    }

    /**
     * Determine whether the given new bean definition is compatible with the
     * given existing bean definition.
     * <p>
     * The default implementation simply considers them as compatible when the
     * bean class name matches.
     * 
     * @param newDefinition the new bean definition, originated from scanning
     * @param existingDefinition the existing bean definition, potentially an
     * explicitly defined one or a previously generated one from scanning
     * @return whether the definitions are considered as compatible, with the
     * new definition to be skipped in favor of the existing definition
     */
    protected boolean isCompatible(BeanDefinition newDefinition, BeanDefinition existingDefinition) {
        return (!(existingDefinition instanceof ScannedGenericBeanDefinition) || // explicitly
                                                                                 // registered
                                                                                 // overriding
                                                                                 // bean
                newDefinition.getSource().equals(existingDefinition.getSource()) || // scanned
                                                                                    // same
                                                                                    // file
                                                                                    // twice
        newDefinition.equals(existingDefinition)); // scanned equivalent class
                                                   // twice
    }

    /**
     * Get the Environment from the given registry if possible, otherwise return
     * a new StandardEnvironment.
     */
    private static Environment getOrCreateEnvironment(BeanDefinitionRegistry registry) {
        Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
        if (registry instanceof EnvironmentCapable) {
            return ((EnvironmentCapable) registry).getEnvironment();
        }
        return new StandardEnvironment();
    }

    public boolean isIncludeAnnotationConfig() {
        return includeAnnotationConfig;
    }

}
